package hyl.ext.ws;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.websocket.Session;

import com.alibaba.fastjson2.JSON;

import hyl.core.MyFun;
import hyl.core.data.ExMap;
import hyl.core.info.Content;
import hyl.ext.ws.msg.Msgpg;
import hyl.ext.ws.msg.WsMsgS;

/**
 * WsServe 通信基础对象
 * 
 * 子类有  WsServe2(双层模型) WsServe3(三层模型) WsServeK(客服模型)等
 * 
 * 包含集合对象:
 * 
 * 	在线终端集: 用户登录终端 ,一个用户可能有多个登录的终端
 * 
 * 	_房间集 : 一个房间对应一个用户或者一个终端
 * 
 * 	_主题管理器: 一个主题可能有多个用户或多个终端
 * 
 * _clientid: 称为客户编号(也叫会话编号或终端编号)
 * 
 * tokenid : 称为令牌  可以是 ws.session.sessionid 也可以是http.session.sessionid,或者自定义的uuid
 * 
 * RID :称为 房号或者组编号   一个房编号 包含多个客户编号
 * 
 * 说明:
 * 
 * 规定聊天组件不缓存用户聊天记录,如果要聊天 可以通过接口 拦截消息,在外部程序中保存聊天记录
 * 
 * 特别注意:客户编号和组编号是互斥的,也就是客户编号和组编号不能有重复
 * 
 * 如果三层模型和双层模型同时启用,那么
 * 
 * 最好三层模型的编号前缀与双层模型编号前缀 必须不同
 * 
 * 原理: _在线终端集 和 _客户集 的key 就是 客户编号或者组编号 如果相同,会互斥
 * 
 * 这种现象只在 两种模型混用时才有 另外如果 三层模型的编号被双层模型终端登录, 会被降层使用
 * 
 * @author 37798955@qq.com
 *
 */
public class WsServe {
	// 发送文本时需要缓存
//		static MyCert _密钥对 = MyCert.getInstance();
//		static {
//			_密钥对.load密钥文本文件("./cert");
//		}
	// static MyRsa _rsa=MyRsa.
	public static final int I过期 = 1800000; // 60s 心跳间隔时间
	/**
	 * key: sessionid, value:会话对象
	 */
	public static ExMap<String, WsServe> _在线终端集 = new ExMap<>(I过期);

	/** 客户端编号==终端编号 */
	public String getClientid() {
		return _clientid;
	}

	/**
	 * 是否使用房间集 取决于模型
	 * 
	 * 客户级中的 WsRoom 也可以是WsRoomMen 取决于
	 * 
	 * 是三层模型还是双层模型
	 * 
	 * 客户编号和房间编号互斥 ,不能重复
	 * 
	 * 如果三层模型和双层模型同时启用,那么
	 * 
	 * 最好三层模型的编号前缀与双层模型编号前缀 必须不同,确保编码不同
	 * 
	 * key: 房号|客户号|终端号 ,value: 房间 启动时加载所有客户信息 ?
	 * 
	 * 用到时加载? 如果加载很多就会很慢 可以考虑用时加载
	 * 
	 * _在线房间集 是会过期的,但是房间数据不过期 实现原理是 把房间的数据序列化,用时在重新载入
	 * 
	 * 
	 * 
	 */
	public static ExMap<String, WsRoom> _在线房间集 = new ExMap<>(I过期);
	/**
	 * 订阅各类主题的客户端(终端)
	 */
	public static TopicManager _主题管理器 = new TopicManager();

	/**
	 * 会话id 一个客户(终端)可以有多个会话,一个会话必然一个客户(终端)
	 */
	protected String _clientid;
	/**
	 * 令牌方式登录,一块令牌只能一个人登录
	 */
	public String _token;
	/**
	 * 客户端ip
	 */
	protected InetSocketAddress _ip;
	/**
	 * 附属信息
	 */
	public Map<String, Object> _info = new HashMap<>(); // 附属信息

	public String toString() {
		return MyFun.join("clientid=", _clientid, ",_token=", _token, ",_ip=", _ip);
	}

	// 与某个客户端的连接会话，需要通过它来给客户端发送数据
	protected Session _Ws会话;
	/**
	 * ping pong 函数
	 */
	public void pong() {
		try {
			_Ws会话.getBasicRemote().sendPong(null);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	// 默认onopen事件
	protected void onopen(Session session) {
		_Ws会话 = session;
		_clientid = session.getId();
		sendTo(_clientid);
	}

	/**
	 * 使用公钥登录的直接 使用sessionid 登录
	 * 
	 * @param session
	 */
	protected void onOpenByPubKey(Session session) {
		_Ws会话 = session;
		_clientid = session.getId();
		sendTo(_clientid);

	}

	/**
	 * 登录之后退出 *
	 * 
	 * @return
	 */
	public void close() {
		try {
			if (_Ws会话 != null)
				_Ws会话.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 很多网上WebSocket服务端代码对于生产环境应用来讲都误导了大家,<br>
	 * 
	 * onClose方法和onError方法处理一模一样.但实际这两个方法分别是不同情况的回调.<br>
	 * 
	 * 一个是关闭一个是异常.虽然很多时候触发onError方法后会触发onClose.<br>
	 * 
	 * 比如网络异常导致连接异常,然后ws关闭了连接.但是也有一些情况是仅触发onError方法.<br>
	 * 
	 * 比如上边的server端close掉连接,然后接到RST包这种情况.<br>
	 * 
	 * 所以我们的处理方案是在onError回调中仅打印一条日志或者针对不同的异常写逻辑.<br>
	 * 
	 * 无论怎么处理都不可以在onError方法中接触userId和session之间的对应关系.<br>
	 * 
	 * 
	 * @param session
	 * @param error
	 */
	public void onError(Session session, Throwable error) {

	}

	/**
	 * 获取终端
	 * 
	 * @param id
	 * @return
	 */
	public static WsServe getClient(String 会话id) {
		return _在线终端集.get(会话id);
	}

	/*
	 * 发送 函数重载 
	 * 
	 */
	
	
	public synchronized boolean sendTo(Object data) {
		if (data == null)
			return false;
		if (data instanceof String) {
			return sendTo((String) data);
		} else if (data instanceof ByteBuffer) {
			return sendTo((ByteBuffer) data);
		} else if (data instanceof byte[]) {
			return sendTo((byte[]) data);
		} else if (data instanceof Content) {
			return sendTo((Content) data);
		} else if (data instanceof WsMsgS) {
			return sendTo((WsMsgS) data);
		} else if (data instanceof Msgpg) {
			return sendTo((Msgpg) data);
		}
		return false;
	}

	/**
	 * 
	 * 作为模型尽量不要使用该函数 发送文本 发送文本字符串
	 * 
	 * 如果不成功 要保存到本地文件
	 * 
	 * 等待用户上线时再发送
	 * 
	 * @param text
	 * @return
	 */
	protected synchronized boolean sendTo(String text) {
		if (_Ws会话 == null || MyFun.isEmpty(text))
			return false;
		try {
			_Ws会话.getBasicRemote().sendText(text);
			return true;
		} catch (IOException e) {
			return false;
		}
		// return sendTo(text.getBytes(Amy.charset));
	}

	/**
	 * 发送字节数组
	 * 
	 * 字节流数据不缓存到本地文件目录
	 * 
	 * @param data
	 * @return
	 */
	protected synchronized boolean sendTo(byte[] data) {
		if (data == null)
			return false;
		return sendTo(ByteBuffer.wrap(data));
	}

	/**
	 * 发送字节流
	 * 
	 * 字节流数据不缓存到本地文件目录
	 * 
	 * @param data
	 * @return
	 */
	protected synchronized boolean sendTo(ByteBuffer data) {
		try {
			if (_Ws会话 == null || data == null || data.remaining() == 0)
				return false;
			_Ws会话.getBasicRemote().sendBinary(data);

			return true;
		} catch (IOException e) {
			return false;
		}
	}

	/**
	 * @param code
	 * @param msg
	 * @return
	 */
	public synchronized boolean sendTo(int code, String msg) {
		Content content = new Content();
		content.setCode(code, msg);
		return sendTo(content);
	}

	public synchronized boolean sendTo(Content ct) {
		return sendTo(ct.toJsonString());
	}

	public synchronized boolean sendTo(WsMsgS ct) {
		ct.set发送方(_clientid);
		return sendTo(ct.toSendString());
	}

	/**
	 * 发送 Msgpg 包
	 * 
	 * 客户端要配套解析
	 * 
	 * @param ct
	 * @return
	 */
	public synchronized boolean sendTo(Msgpg ct) {
		return sendTo(ct.toSendBytes());
	}

	/////////////////////////////////// 以下函数适用于 系统对用户或用户组的发送////////////

	public static ExMap<String, WsServe> getClients() {
		return _在线终端集;
	}

	/**
	 * 广播,只能对在线客户端广播
	 */
	public static String broadcast(String 内容) {
		if (MyFun.isEmpty(内容))
			return WsUtil.er内容不能为空;
		_在线终端集.forValue((cc) -> {
			cc.sendTo(内容);
		});
		return WsUtil.成功;
	}

	/**
	 * 发送在线广播 消息
	 * 
	 * @throws IOException
	 */
	public static String broadcast(Content 内容) {
		if (内容 == null)
			return WsUtil.er内容不能为空;
		_在线终端集.forValue((cc) -> {
			cc.sendTo(内容);
		});
		return WsUtil.成功;
	}

	public static String broadcast(byte[] 内容) {
		if (内容 == null)
			return WsUtil.er内容不能为空;
		_在线终端集.forValue((cc) -> {
			cc.sendTo(内容);
		});
		return WsUtil.成功;
	}

	//////////////////////////////// 系统常用指令///////////////////////////

	//////////////////////////////// 测试用
	//////////////////////////////// 文本指令/////////////////////////////////////////////
	public final static String CMD_推送个人2 = "推送个人2";
	public final static String CMD_推送主题2 = "推送主题2";
	public final static String CMD_广播2 = "广播2";

	public final static String CMD_推送个人3 = "推送个人3";
	public final static String CMD_推送主题3 = "推送主题3";
	public final static String CMD_广播3 = "广播3";

	public final static String CMD_订阅主题 = "订阅主题";
	public final static String CMD_退订主题 = "退订主题";
	public final static String CMD_查主题订阅人 = "查主题订阅人";
	public final static String CMD_查客户集 = "查客户集";
	public final static String CMD_查终端集 = "查终端集";

	public static void stop(String 终端id) {
		_在线终端集.get(终端id).close();
	}

	public static String cmd(String 指令, String 接收方, String 消息) {
		if (CMD_推送个人2.equals(指令)) {
			WsServe2.sendTo(接收方, 消息);
		} else if (CMD_推送主题2.equals(指令)) {
			WsServe2.sendTopic(接收方, 消息);
		} else if (CMD_广播2.equals(指令)) {
			WsServe2.broadcast(消息);
		} else if (CMD_推送个人3.equals(指令)) {
			WsServe3.sendTo(接收方, 消息);
		} else if (CMD_推送主题3.equals(指令)) {
			WsServe3.sendTopic(接收方, 消息);
		} else if (CMD_广播3.equals(指令)) {
			WsServe3.broadcast(消息);
		} else if (CMD_订阅主题.equals(指令)) {
			WsServe._主题管理器.f订阅主题(接收方, 消息);
		} else if (CMD_退订主题.equals(指令)) {
			WsServe._主题管理器.f取消订阅(接收方, 消息);
		} else if (CMD_查主题订阅人.equals(指令)) {
			Map<String, Set<String>> jjMap = _主题管理器.get所有主题的订阅人();
			return JSON.toJSONString(jjMap);
		} else if (CMD_查客户集.equals(指令)) {
			return JSON.toJSONString(_在线房间集);
		} else if (CMD_查终端集.equals(指令)) {
			return JSON.toJSONString(_在线终端集);
		}
		return "ok";
	}

}
